<?php

/**
 * Records changes made to an object during save operations.
 */
class EventLogBehavior extends ModelBehavior {
  /**
   * A copy of the object as it existed prior to the save. We're going
   * to store this off so we can calculate the deltas after save.
   *
   * @var   Object
   */
  private $_original = array();

  /**
   * Initiate behavior for the model using specified settings.
   *
   * Available settings:
   *   - ignore_fileds array, optional
   *            An array of property names to be ignored when records
   *            are created in the deltas table.
   *   - habtm  array, optional
   *            An array of models that have a HABTM relationship with
   *            the acting model and whose changes should be monitored
   *            with the model.
   */
  public function setup( Model $Model, $settings = array() ) {
    if( !isset( $this->settings[$Model->alias] ) ) {
      $this->settings[$Model->alias] = array(
        'ignore_fileds' => array( 'created', 'updated', 'modified' ),
      	'ignore_event' => array(),
        'habtm'  => count( $Model->hasAndBelongsToMany ) > 0
          ? array_keys( $Model->hasAndBelongsToMany )
          : array()
      );
    }
    if( !is_array( $settings ) ) {
      $settings = array();
    }
    $this->settings[$Model->alias] = array_merge_recursive( $this->settings[$Model->alias], $settings );

    foreach( $this->settings[$Model->alias]['habtm'] as $index => $model_name ) {
      if( !array_key_exists( $model_name, $Model->hasAndBelongsToMany )  ) {
        unset( $this->settings[$Model->alias]['habtm'][$index] );
      }
    }
  }

  /**
   * Executed before a save() operation.
   *
   * @return  boolean
   */
  public function beforeSave( Model $Model ) {
    # If we're editing an existing object, save off a copy of
    # the object as it exists before any changes.
    if( !empty( $Model->id ) ) {
      $this->_original[$Model->alias] = $this->_getModelData( $Model );
    }
    
    return true;
  }
  
  /**
   * Executed before a delete() operation.
   *
   * @param 	$Model
   * @return	boolean
   */
  public function beforeDelete( Model $Model, $cascade = true ) {
    $original = $Model->find(
      'first',
      array(
        'contain'    => false,
        'conditions' => array( $Model->alias . '.' . $Model->primaryKey => $Model->id ),
      )
    );
    $this->_original[$Model->alias] = $original[$Model->alias];
    
    return true;
  }

  /**
   * function afterSave
   * Executed after a save operation completes.
   *
   * @param   $created  Boolean. True if the save operation was an
   *                    insertion. False otherwise.
   * @return  void
   */
  public function afterSave( Model $Model, $created ) {
    $audit = array( $Model->alias => $this->_getModelData( $Model ) );
    $audit[$Model->alias][$Model->primaryKey] = $Model->id;
    
    $event = $created ? 'CREATE' : 'EDIT';
    
    
    if(in_array($event,$this->settings[$Model->alias]['ignore_event'])){
    	return true;
    }

    
    $update_fields = $before_data = $after_data = array();
    if( !$created && is_array($this->_original[$Model->alias]) ){ // 仅修改时，保存变化的值。
	    foreach($this->_original[$Model->alias] as $key => $val){
	    	if( !in_array($key,$this->settings[$Model->alias]['ignore_fileds']) && $val != $audit[$Model->alias][$key] ) {	    		
	    		$update_fields[] = $key;	    		
	    		$before_data[$key] = $val;
	    		$after_data[$key] = $audit[$Model->alias][$key];
	    	}
	    }
    }
    elseif( $created ) {
    	$after_data = $audit[$Model->alias];
    }
    // if(empty($before_data) && empty($after_data))
    
    /*
     * Create a runtime association with the Audit model and bind the
     * Audit model to its AuditDelta model.
     */
    $Model->bindModel(
      array( 'hasMany' => array( 'EventLog' ) )
    );
    
    // wxpay_notify do not has session data.
    $user_id = $audit[$Model->alias]['user_id'] ? $audit[$Model->alias]['user_id'] : $audit[$Model->alias]['creator'];
    $user_id = $user_id ? $user_id :  CakeSession::read('Auth.User.id');
    
    $data = array(
      'EventLog' => array(
        'event'     => $event,
        'model'     => $Model->alias,
        'data_id' => $Model->id,
        'before_data' => json_encode( $before_data ),
      	'after_data' => json_encode( $after_data ),
      	'created' => time(),
      	'user_id' => $user_id,
      	'url' => Router::url(),
      )
    );

    # Insert an audit record if a new model record is being created
    # or if something we care about actually changed.
    if( $created || count( $update_fields ) ) { // 创建了数据成功，或者修改时修改了字段。
      $Model->EventLog->create();
      $Model->EventLog->save( $data );
    }
    $Model->unbindModel(
      array( 'hasMany' => array( 'EventLog' ) )
    );

    if( isset( $this->_original ) ) {
      unset( $this->_original[$Model->alias] );
    }
    return true;    
  }
  
  /**
   * Executed after a model is deleted.
   *
   * @param 	$Model
   * @return	void
   */
  public function afterDelete( Model $Model ) {
        
    $audit = array( $Model->alias => $this->_original[$Model->alias] );
    
    // wxpay_notify do not has session data.
    $user_id = $audit[$Model->alias]['user_id'] ? $audit[$Model->alias]['user_id'] : $audit[$Model->alias]['creator'];
    $user_id = $user_id ? $user_id :  CakeSession::read('Auth.User.id');
    
    $data  = array(
      'EventLog' => array(
        'event'   => 'DELETE',
        'model'   => $Model->alias,
        'data_id'   => $Model->id,
        'before_data' => json_encode( $audit ),
      	'created' => time(),
      	'user_id' => $user_id,
      	'url' => Router::url(),
      )
    );
    
    $this->EventLog = ClassRegistry::init( 'EventLog' );
    $this->EventLog->create();
    $this->EventLog->save( $data );
  }

  /**
   * function _getModelData
   * Retrieves the entire set model data contained to the primary
   * object and any/all HABTM associated data that has been configured
   * with the behavior.
   *
   * Additionally, for the HABTM data, all we care about is the IDs,
   * so the data will be reduced to an indexed array of those IDs.
   *
   * @param   $Model
   * @return  array
   */
  private function _getModelData( Model $Model ) {
    /*
     * Retrieve the model data along with its appropriate HABTM
     * model data.
     */
    $data = $Model->find(
      'first',
      array(
        'contain' => !empty( $this->settings[$Model->alias]['habtm'] )
          ? array_values( $this->settings[$Model->alias]['habtm'] )
          : array(),
        'conditions' => array( $Model->alias . '.' . $Model->primaryKey => $Model->id )
      )
    );

    $audit_data = array(
      $Model->alias => $data[$Model->alias]
    );

    foreach( $this->settings[$Model->alias]['habtm'] as $habtm_model ) {
      if( array_key_exists( $habtm_model, $Model->hasAndBelongsToMany ) && isset( $data[$habtm_model] ) ) {
        $habtm_ids = Set::combine(
          $data[$habtm_model],
          '{n}.id',
          '{n}.id'
        );
        /*
         * Grab just the id values and sort those
         */
        $habtm_ids = array_values( $habtm_ids );
        sort( $habtm_ids );

        $audit_data[$Model->alias][$habtm_model] = implode( ',', $habtm_ids );
      }
    }

    return $audit_data[$Model->alias];
  }
}
